June 04, 2021
React
μ»΄ν¬λνΈμ ꡬμ±μμκΉμ§ DOMμΌλ‘ λ§λ€μ΄μ ν
μ€νΈλ₯Ό ν μ μκ² λμμ£Όλ λΌμ΄λΈλ¬λ¦¬ react-testing-library
μ λ³λμ μ¬λ¬ λΌμ΄λΈλ¬λ¦¬λ₯Ό λ°μμ¬ νμ μμ΄ νλλ‘ ν©μ³μ§ Jest
λ₯Ό ν΅ν΄ ν
μ€νΈ μ½λ μμ±
μ¦, μλ‘κ° λ€λ₯Έ μν μ κ°κ³ μκ³ λμμ£Όλ νν
λ¬Όλ‘ react-testing-library
λ§ κ°κ³ ν
μ€νΈλ₯Ό μν Mocha
, Expect
κ°μ λΌμ΄λΈλ¬λ¦¬λ₯Ό μλμΌλ‘ λ°μμ μμ
ν μ μλ€κ³ ν¨. νμ§λ§ Jest
λ§ λ°μΌλ©΄ λͺ¨λ κ² λ¨ create-react-app
μ λκ°μ§κ° μ€μΉλ μνλ‘ λμ΄μ΄
ν μ€νΈλ₯Ό μν μ¬λ¬ λΌμ΄λΈλ¬λ¦¬λ₯Ό ν©μΉ μνλ‘ λ±μ₯νλ©΄μ νλμ ν μ€νΈ νλ μμν¬λΌ λΆλ¦¬κ³ μμ.
Jestμμ Matchers
λ κ²°κ³Όκ°μ ν
μ€νΈν μ μλ μ¬λ¬ API
λ€μ μλ―Ένλ€.
κΈ°λ³Έμ μΌλ‘ expect().Matcher APIs
νμμΌλ‘ μ§νλλ λλμ΄λ€.
expect()
λ‘ ν
μ€νΈ μ§νμ λμμ΄ λλ μμ
(λ©μλ, μ»΄ν¬λνΈ)λ€μ΄ λ€μ΄κ°κ³ ν΄λΉ λ©μλλ μ€μ κ²°κ³Όλ¬ΌμΈ expection
κ°μ²΄λ₯Ό λ°ννλ€κ³ νλ€.
μ΄νμ Matcher APIs
λ₯Ό ν΅ν΄ λ΄κ° μμνκ³ μνλ κ²°κ³Όλ¬Όκ³Ό λΉκ΅νλκ² κ°λ€.
μ¬λ¬κ°μ§ λΉκ΅ API
λ€μ΄ μκ³ promise
, async await
κ³Ό κ°μ λΉλκΈ°μμ
μ ν
μ€νΈλ κ°λ₯νλ€.
React
μ λΌμ΄νμ¬μ΄ν΄κ³Ό κ°μ μμκ° ν
μ€νΈ νμΌμλ μ‘΄μ¬νλκ² κ°λ€.
beforeAll
: ν΄λΉ ν
μ€νΈ νμΌ λ΄ λͺ¨λ ν
μ€νΈ μμ
μ μ νλ²λ§ μ€νλλ λ©μλbeforeEach
: κ°κ°μ λͺ¨λ ν
μ€νΈ μμ
μ΄ μ μ μ€νλλ λ©μλafterAll
: ν΄λΉ ν
μ€νΈ νμΌ λ΄ λͺ¨λ ν
μ€νΈ μμ
μ’
λ£ νμ νλ²λ§ μ€νλλ λ©μλbeforeAll
: κ°κ°μ λͺ¨λ ν
μ€νΈ μμ
μ’
λ£ νμ μ€νλλ λ©μλλν, ν
μ€νΈ λ΄μ μ€μ½ν(μμ)μ μ§μ ν΄ μ€ μ μλ λ©μλλ μ‘΄μ¬νλ€.
describe
λ₯Ό ν΅ν΄, μ€μ½νλ₯Ό μ§μ ν΄μ£Όκ³ μμ λΌμ΄νμ¬μ΄ν΄λ λ΄λΆμμ λ³λλ‘ λμν μ μμλ€,
beforeAll(() => console.log('1 - beforeAll'))
afterAll(() => console.log('1 - afterAll'))
beforeEach(() => console.log('1 - beforeEach'))
afterEach(() => console.log('1 - afterEach'))
test('', () => console.log('1 - test'))
describe('Scoped / Nested block', () => {
beforeAll(() => console.log('2 - beforeAll'))
afterAll(() => console.log('2 - afterAll'))
beforeEach(() => console.log('2 - beforeEach'))
afterEach(() => console.log('2 - afterEach'))
test('', () => console.log('2 - test'))
})
// 1 - beforeAll
// 1 - beforeEach
// 1 - test
// 1 - afterEach
// 2 - beforeAll
// 1 - beforeEach
// 2 - beforeEach
// 2 - test
// 2 - afterEach
// 1 - afterEach
// 2 - afterAll
// 1 - afterAll
mocking
μ λ¨μ ν
μ€νΈλ₯Ό μμ±ν λ, ν΄λΉ μ½λκ° μμ‘΄νλ λΆλΆμ κ°μ§(mock
)λ‘ λ체νλ κΈ°λ²μ΄λΌκ³ νλ€. μ무λλ ν
μ€νΈ νλ €λ μ½λκ° μμ‘΄νλ λΆλΆμ μ§μ μμ±νλκ²μ λΆλ΄μ€λ½κΈ° λλ¬Έμ mocking
μ λ§μ΄ μ¬μ©νλ€κ³ νλ€.
μλ‘, λ°μ΄ν°λ² μ΄μ€μ crud
μμ
μ λν ν
μ€νΈλ₯Ό μμ±ν λ μ€μ λ°μ΄ν°λ² μ΄μ€λ₯Ό μ¬μ©νκ²λλ€λ©΄ μ¬λ¬ λΆνΈν¨κ³Ό λ¬Έμ κ° μμ μ μλ€.
λν Unit Test(λ¨μν
μ€νΈ)
λ μΈλΆνκ²½μ μμ‘΄νμ§ μκ³ λ
립μ μΌλ‘ μ€νλμΌνλ€λ μ μμ μλ°°λλ€.
μ΄λ΄ λ, κ°μ§ κ°μ²΄λ₯Ό μμ±νμ¬ ν
μ€νΈλ₯Ό μ§ννλκ²μ΄λ€.
μ»΄ν¬λνΈμ props
λ‘ λ°μμ¨ λ©μλ λν, μ΄κΈ° ν
μ€νΈ render
λ mocking
ν¨μλ₯Ό 보λ΄μ΄ μ€νλλμ§ νμΈν μ μμλ€.
// Link
const Link = ({ page, setState, children }: Props) => {
const [status, setStatus] = useState(STATUS.NORMAL)
const onMouseEnter = useCallback(() => {
setStatus(STATUS.HOVERED)
setState('μ€νλμλΉ')
}, [setStatus, STATUS, setState])
const onMouseLeave = useCallback(() => {
setStatus(STATUS.NORMAL)
}, [setStatus, STATUS])
return (
<a
className={status}
href={page || '#'}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{children}
</a>
)
}
// test
describe('Link', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('Link component mount', () => {
// screen.debug();
})
it('onMouse Toggle', () => {
const setState = jest.fn()
render(
<Link page="http://www.facebook.com" setState={setState}>
Facebook
</Link>
)
const target = screen.getByText('Facebook')
fireEvent.mouseEnter(target)
console.log(target.className)
expect(setState).toBeCalledTimes(1)
fireEvent.mouseLeave(target)
console.log(target.className)
})
})
μμ±λ mocking
ν¨μ μΈμ€ν΄μ€μλ .mock
μμ±μ΄ μ‘΄μ¬νλ€. ν΄λΉ μΈμ€ν΄μ€λ μλμ κ°μ μμ±λ€μ κ°κ³ μλ€.
mock = {
calls, // μ€νλ λλ§λ€ λ°μ λ³μλ₯Ό λ΄μ μ΄μ€λ°°μ΄
instances, // λ³μκ° μλ λ°μΈλ©λ κ°μ²΄λ±μ μΈμ€ν΄μ€
invocationCallOrder, // ν΄λΉ λͺ¨μν¨μ(mock) μΈμ€ν΄μ€κ° μ€νλ νμ
results, // μ΄κΈ° λͺ¨μν¨μκ° μμ±λ λ 보λ΄μ§ μμ
μ κ²°κ³Όκ°
}
jest.fn()
μ ν΅ν΄ mocking
ν¨μλ₯Ό λ§λ€ μ μλ€. μμ±λ mocking
ν¨μμ κ²°κ³Όκ°μ μ§μ ν΄ μ€ μ μλ€.
describe('Mock fn', () => {
let mockFn = null
beforeEach(() => {
mockFn.mockClear()
mockFn = jest.fn()
})
it('mockReturnValue', () => {
mockFn.mockReturnValue('Mock ν¨μ μ
λλ€.')
console.log(mockFn()) // 'Mock ν¨μ μ
λλ€.'
})
it('mockReturnValueOnce', () => {
mockFn.mockReturnValueOnce(false).mockReturnValueOnce(true)
const result = [11, 12].filter(num => mockFn(num))
console.log(result) // 12
console.log(mockFn.mock)
// {
// calls: [ [ 11 ], [ 12 ] ],
// instances: [ undefined, undefined ],
// invocationCallOrder: [ 1, 2 ],
// results: [ { type: 'return', value: false }, { type: 'return', value: true } ]
// }
})
it('mockResolvedValue', () => {
mockFn.mockResolvedValue('λΉλκΈ° Mock ν¨μ μ
λλ€.')
mockFn().then(res => console.log(res)) // 'λΉλκΈ° Mock ν¨μ μ
λλ€.'
})
it('mockImplementation', () => {
mockFn.mockImplementation(name => `I am ${name}`)
console.log(mockFn('μλ―Ό')) // μλ―Ό
})
it('mockFn props', () => {
mockFn('a')
mockFn(['b', 'c'])
expect(mockFn).toBeCalledTimes(2)
expect(mockFn).toBeCalledWith('a')
expect(mockFn).toBeCalledWith(['b', 'c'])
})
})
jest.spyOn
μ κ²½μ°, μΈλΆ λͺ¨λμ κ°μ§νλ μ€νμ΄λ₯Ό λΆμ¬λλ κ°λ
μ΄κΈ° λλ¬Έμ μνΈ μμ‘΄μ±μ΄ μκΈΈ μ μμ΄ jest.fn()
μΌλ‘ μλ‘μ΄ κ°μμ mocking
λͺ¨λμ λ§λλ κ²μ΄ λ μ’λ€κ³ νλ€.
fetch
, axios
λ±μ κΈ°λ³Έ μλ°μ€ν¬λ¦½νΈ api
μλ μ¬μ© κ°λ₯νλ€.
ν΄λΉ λͺ¨λμ μμ±μ μ κ·Όνλ λ°©μ
const calculator = {
add: (a, b) => a + b,
}
describe('Mock spy', () => {
it('spy fn', () => {
const mockSpy = jest.spyOn(calculator, 'add')
const result = calculator.add(2, 3)
console.log(mockSpy.mock)
expect(mockSpy).toBeCalledTimes(1)
expect(mockSpy).toBeCalledWith(2, 3)
expect(result).toBe(5)
})
})
λΉλκΈ° API
λ₯Ό ν
μ€νΈ νλ μμ μ΄λ€. spyOn
μ μ¬μ©νμ¬ κΈ°μ‘΄μ λ©μλλ₯Ό κ°μ§νλ λ°©μμ΄κΈ° λλ¬Έμ, μλ² μ’
λ£λ μλͺ»λ urlλ± μλ¬κ° λ°μνλ©΄ ν
μ€νΈμ½λκ° μ§νλμ§ μκΈ° λλ¬Έμ, fn
μΌλ‘ μλ‘μ΄ mocking
ν¨μλ₯Ό λ§λ€μ΄μ μ¬μ©νλκ²μ΄ λ
립μ±μ΄ μ€μν μ λν
μ€νΈμ μ ν©νλ€.
describe('Mock function example', () => {
it('get some async data with axios', async () => {
axios.get = jest.fn().mockResolvedValue({ data: { id: 1, name: 'μλ―Ό' } })
const user = await axios.get('some url').then(res => res.data)
console.log(user) // { id: 1, name: 'μλ―Ό' }
axios.get.mockClear()
})
it('get some async data with fetch', async () => {
// console.log(global.fetch); -> [Function: fetch]
global.fetch = jest
.fn()
.mockResolvedValue({ data: { id: 1, name: 'μλ―Ό' } })
global.fetch.mockClear
// console.log(global.fetch); -> [Function: mockConstructor]
const { data } = await fetch('url')
console.log(data) // { id: 1, name: 'μλ―Ό' }
global.fetch.mockClear()
})
})
mocking
ν¨μλ₯Ό λ§λ€κΈ° μ μλ μλ³Έμ λ©μλλ‘ μ‘΄μ¬νμ§λ§, mocking
ν¨μλ₯Ό λ§λ€κ³ λ λ€μ ν΄λΉ μλ³Έ λ©μλλ₯Ό κ²μνλ©΄ [Function: mockConstructor]
λΌλ κ²°κ³Όκ°μ΄ λμ¨λ€.
μ¦, μ΄ ν μ€νΈ μμλ ν΄λΉ λ©μλκ°
mocking
ν¨μλ‘ λ³κ²½λμλ€λκ²μ μλ―Ένλ€κ³ νλ€.
λ°λΌμ, μμ
μ΄ λλ λ€μμλ ν΄λΉ mocking
μΈμ€ν΄μ€λ₯Ό μ΄κΈ°νμμΌμ£Όλλ‘ νμ.
jest.clearAllMocks()
λ‘ λͺ¨λ μ΄κΈ°ννκ±°λ, global.fetch.mockClear()
λ‘ κ°λ³ μ΄κΈ°νλ κ°λ₯νλ€.
// μ΄κΈ°ν μ
global.fetch = {
calls: [['url']],
instances: [undefined],
invocationCallOrder: [2],
results: [{ type: 'return', value: [Promise] }],
}
// μ΄κΈ°ν ν
global.fetch = {
calls: [],
instances: [],
invocationCallOrder: [],
results: [],
}
μμμ fetch
, axios
λ₯Ό mock ν¨μλ‘ λ§λ€μ΄μ μ¬μ©μ νλ€. μ¦, λͺ¨λ λ΄λΆμ μλ λ©μλκ° μ λλ‘ μ μλμ΄μμ§ μκ±°λ λͺ¨λ₯΄λλΌλ μνλ κ²°κ³Όκ°μ μ§ννμ¬ μ΄νμ κ³Όμ μ ν
μ€νΈ ν μ μμλ€.
νμ§λ§, λ§μ½ λͺ¨λ λ΄λΆμ μλ μ¬λ¬κ° μ λ©μλλ₯Ό mock
ν΄μΌνλ€λ©΄ jest.fn()
μ μ¬λ¬λ² λ°λ³΅νμ¬ λ§λ€μ΄μ€μΌ ν κ²μ΄λ€.
μ΄λ₯Ό λμμ£ΌκΈ° μν λ©μλκ° jest.mock(λͺ¨λ)
μ΄λ€. μΈμλ‘ λ³΄λ΄μ§λ λͺ¨λ μ 체λ₯Ό mocking
ν¨μλ‘ λ§λ€μ΄μ£Όλκ²μ΄λ€.
jest.mock(axios)
,jset.mock(../utils.ts)
ν΄λΉ λͺ¨λ λ΄λΆμ λͺ¨λ λ©μλκ°mock
μΈμ€ν΄μ€ν λλ€.
μ΄μ μ axios.get = jest.fn()...
λ°©μμΌλ‘ mocking
ν κ²½μ°, axios
μμ get
μμ±λ§ mock
μΈμ€ν΄μ€κ° λμλλ°, μ΄ λ°©μμΌλ‘ νκ²λ κ²½μ° axios
λ΄λΆμ λͺ¨λ μμ±λ€μ΄ λͺ¨λ mock
μΈμ€ν΄μ€ν λλ€.
axios = {
delete: [Function: wrap] {
_isMockFunction: true,
getMockImplementation: [Function (anonymous)],
mock: [Getter/Setter],
mockClear: [Function (anonymous)],
mockReset: [Function (anonymous)],
mockRestore: [Function (anonymous)],
mockReturnValueOnce: [Function (anonymous)],
mockResolvedValueOnce: [Function (anonymous)],
mockRejectedValueOnce: [Function (anonymous)],
mockReturnValue: [Function (anonymous)],
mockResolvedValue: [Function (anonymous)],
mockRejectedValue: [Function (anonymous)],
mockImplementationOnce: [Function (anonymous)],
mockImplementation: [Function (anonymous)],
mockReturnThis: [Function (anonymous)],
mockName: [Function (anonymous)],
getMockName: [Function (anonymous)]
},
get: [Function: wrap] {
_isMockFunction: true,
getMockImplementation: [Function (anonymous)],
mock: [Getter/Setter],
mockClear: [Function (anonymous)],
mockReset: [Function (anonymous)],
mockRestore: [Function (anonymous)],
mockReturnValueOnce: [Function (anonymous)],
mockResolvedValueOnce: [Function (anonymous)],
mockRejectedValueOnce: [Function (anonymous)],
mockReturnValue: [Function (anonymous)],
mockResolvedValue: [Function (anonymous)],
mockRejectedValue: [Function (anonymous)],
mockImplementationOnce: [Function (anonymous)],
mockImplementation: [Function (anonymous)],
mockReturnThis: [Function (anonymous)],
mockName: [Function (anonymous)],
getMockName: [Function (anonymous)]
},
}
μ΄μ μ λ§λ€μλ νλ‘μ νΈλ€μ ν
μ€νΈμ½λλ₯Ό νλ² μ
νλ³Ό μκ°μ΄λ€. λν, μ€μ μμ
μ΄μ μ ν
μ€νΈμ½λλ₯Ό λ¨Όμ μμ±νκ³ , μ€ν¨λλ ν
μ€νΈμ½λλ₯Ό νλνλ μ±κ³΅μΌλ‘ λ§μΆ°λκ°λ TDD
κ°λ°λ‘ μ λν΄ νλ² μμλ΄μΌν κ² κ°λ€.